import os
import copy
import datetime
import logging
from typing import Any, Dict, List, Optional, Text

import rasa.nlu
import rasa.utils.io
from rasa.constants import MINIMUM_COMPATIBLE_VERSION
from rasa.nlu import components, utils  # pytype: disable=pyi-error
from rasa.nlu.components import Component, ComponentBuilder  # pytype: disable=pyi-error
from rasa.nlu.config import RasaNLUModelConfig, component_config_from_pipeline
from rasa.nlu.persistor import Persistor
from rasa.nlu.training_data import Message, TrainingData
from rasa.nlu.utils import write_json_to_file

MODEL_NAME_PREFIX = "nlu_"

logger = logging.getLogger(__name__)


class InvalidModelError(Exception):
    """在模型加载失败时引发。

    Attributes:
        message -- 解释为什么模型无效
    """

    def __init__(self, message: Text) -> None:
        self.message = message

    def __str__(self) -> Text:
        return self.message


class UnsupportedModelError(Exception):
    """在模型太旧而无法加载时引发。

    Attributes:
        message -- 解释为什么模型无效
    """

    def __init__(self, message: Text) -> None:
        self.message = message

    def __str__(self) -> Text:
        return self.message


class Metadata:
    """捕获有关模型的所有信息以进行加载和准备。"""

    @staticmethod
    def load(model_dir: Text):
        """从模型目录加载元数据。
        Args:
            model_dir: 保存模型的目录。
        Returns:
            Metadata: 描述模型的元数据对象
        """
        try:
            metadata_file = os.path.join(model_dir, "metadata.json")
            data = rasa.utils.io.read_json_file(metadata_file)
            return Metadata(data, model_dir)
        except Exception as e:
            abspath = os.path.abspath(os.path.join(model_dir, "metadata.json"))
            raise InvalidModelError(
                f"Failed to load model metadata from '{abspath}'. {e}"
            )

    def __init__(self, metadata: Dict[Text, Any], model_dir: Optional[Text]):

        self.metadata = metadata
        self.model_dir = model_dir

    def get(self, property_name: Text, default: Any = None) -> Any:
        return self.metadata.get(property_name, default)   # 该get函数服务于@property私有函数

    @property
    def component_classes(self):
        if self.get("pipeline"):
            return [c.get("class") for c in self.get("pipeline", [])]
        else:
            return []

    @property
    def number_of_components(self):
        return len(self.get("pipeline", []))

    def for_component(self, index: int, defaults: Any = None) -> Dict[Text, Any]:
        return component_config_from_pipeline(index, self.get("pipeline", []), defaults)

    @property
    def language(self) -> Optional[Text]:
        """底层模型的语言"""
        return self.get("language")

    def persist(self, model_dir: Text):
        """将模型的元数据持久化到给定目录。"""
        metadata = self.metadata.copy()

        metadata.update(
            {
                "trained_at": datetime.datetime.now().strftime("%Y%m%d-%H%M%S"),
                "rasa_version": rasa.__version__,
            }
        )

        filename = os.path.join(model_dir, "metadata.json")
        write_json_to_file(filename, metadata, indent=4)


class Trainer:
    """训练器将加载数据并训练所有组件。
    提前需要：用于训练的pipeline格式和configuration配置。
    """

    def __init__(
        self,
        cfg: RasaNLUModelConfig,
        component_builder: Optional[ComponentBuilder] = None,
        skip_validation: bool = False,
    ):

        self.config = cfg
        self.skip_validation = skip_validation
        self.training_data = None  # type: Optional[TrainingData]

        # 如果未传递任何构建器，则每次解释器创建都会产生一个新的构建器。 因此，没有组件被重用。
        if component_builder is None:
            component_builder = components.ComponentBuilder()

        # 在实例化组件类之前，根据config配置中的组件名（必须和该类的名字相同）检查是否所有必需的软件包都可用
        if not self.skip_validation:
            components.validate_requirements(cfg.component_names)

        # 构建pipeline
        self.pipeline = self._build_pipeline(cfg, component_builder)

    def _build_pipeline(
        self, cfg: RasaNLUModelConfig, component_builder: ComponentBuilder
    ) -> List[Component]:
        """将传递的管道组件名称转换为类"""

        # 管道的组件类收集：
        # （1）组件config.yml自定义覆盖类内的默认设置 （2）pipline添加组件对象 （3）带有cache_key的缓存到ComponentBuilder
        pipeline = []
        for i in range(len(cfg.pipeline)):
            component_cfg = cfg.for_component(i)
            component = component_builder.create_component(component_cfg, cfg)
            pipeline.append(component)

        # 组件有效性验证
        if not self.skip_validation:
            components.validate_pipeline(pipeline)

        return pipeline

    def train(self, data: TrainingData, **kwargs: Any) -> "Interpreter":
        """使用提供的训练数据训练管道组件。"""

        self.training_data = data

        self.training_data.validate()

        # 用于存储上下文都会用到的词嵌入向量，共享文件路径等
        context = kwargs

        for component in self.pipeline:
            updates = component.provide_context()
            if updates:
                context.update(updates)

        # 训练开始之前：检查是否提供了所有参数
        if not self.skip_validation:
            components.validate_required_components_from_data(
                self.pipeline, self.training_data
            )

        # 数据在训练期间会在内部进行修改-因此拷副本
        working_data = copy.deepcopy(data)

        # 各个组件在此处开始训练
        for i, component in enumerate(self.pipeline):
            logger.info(f"Starting to train component {component.name}")
            component.prepare_partial_processing(self.pipeline[:i], context)
            updates = component.train(working_data, self.config, **context)
            logger.info("Finished training component.")
            if updates:
                context.update(updates)

        return Interpreter(self.pipeline, context)

    @staticmethod
    def _file_name(index: int, name: Text) -> Text:
        return f"component_{index}_{name}"

    def persist(
        self,
        path: Text,
        persistor: Optional[Persistor] = None,
        fixed_model_name: Text = None,
        persist_nlu_training_data: bool = False,
    ) -> Text:
        """将管道的所有组件持久化到传递的路径。 返回持久化模型的目录。"""

        timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
        metadata = {"language": self.config["language"], "pipeline": []}

        if fixed_model_name:
            model_name = fixed_model_name
        else:
            model_name = MODEL_NAME_PREFIX + timestamp

        path = os.path.abspath(path)
        dir_name = os.path.join(path, model_name)

        rasa.utils.io.create_directory(dir_name)

        if self.training_data and persist_nlu_training_data:
            metadata.update(self.training_data.persist(dir_name))

        for i, component in enumerate(self.pipeline):
            file_name = self._file_name(i, component.name)
            update = component.persist(file_name, dir_name)
            component_meta = component.component_config
            if update:
                component_meta.update(update)
            component_meta["class"] = utils.module_path_from_object(component)

            metadata["pipeline"].append(component_meta)

        Metadata(metadata, dir_name).persist(dir_name)

        if persistor is not None:
            persistor.persist(dir_name, model_name)
        logger.info(
            "Successfully saved model into '{}'".format(os.path.abspath(dir_name))
        )
        return dir_name


class Interpreter:
    """使用经过训练的组件管道来解析文本消息。"""

    @staticmethod
    def default_output_attributes() -> Dict[Text, Any]:
        """定义所有属性（和默认值），并将由`parse`函数返回，其他的组件属性将追加到该字典中"""
        return {"intent": {"name": None, "confidence": 0.0},
                "entities": []
                }

    @staticmethod
    def ensure_model_compatibility(
        metadata: Metadata, version_to_check: Optional[Text] = None
    ) -> None:
        from packaging import version

        if version_to_check is None:
            version_to_check = MINIMUM_COMPATIBLE_VERSION

        model_version = metadata.get("rasa_version", "0.0.0")
        if version.parse(model_version) < version.parse(version_to_check):
            raise UnsupportedModelError(
                "The model version is too old to be "
                "loaded by this Rasa NLU instance. "
                "Either retrain the model, or run with "
                "an older version. "
                "Model version: {} Instance version: {}"
                "".format(model_version, rasa.__version__)
            )

    @staticmethod
    def load(
        model_dir: Text,
        component_builder: Optional[ComponentBuilder] = None,
        skip_validation: bool = False,
    ) -> "Interpreter":
        """基于持久化模型创建解释器。

        Args:
            skip_validation:    如果设置为True，则在加载组件之前尝试检查是否已安装了组件所需的所有软件包。
            model_dir:          要加载的模型的路径
            component_builder:  要使用的:class:`rasa.nlu.components.ComponentBuilder`

        Returns:
           使用加载的模型的解释器
        """

        # 加载metadata.json元数据
        model_metadata = Metadata.load(model_dir)

        # 版本检查：确保兼容性
        Interpreter.ensure_model_compatibility(model_metadata)
        return Interpreter.create(model_metadata, component_builder, skip_validation)

    @staticmethod
    def create(
        model_metadata: Metadata,
        component_builder: Optional[ComponentBuilder] = None,
        skip_validation: bool = False,
    ) -> "Interpreter":
        """加载由元数据定义的存储好的模型和组件。"""

        context = {}   # 上下文信息：模型对象+模型文件
        pipeline = []  # 管道：组件类对象

        # 在实例化组件类之前，检查是否所有必需的软件包都可用
        if not skip_validation:
            components.validate_requirements(model_metadata.component_classes)

        # 如果未传递任何构建器，则每次解释器创建都会产生一个新的构建器。 因此，没有组件被重用。
        if component_builder is None:
            component_builder = components.ComponentBuilder()

        # 遍历组件，加载到组件构建器对象
        for i in range(model_metadata.number_of_components):
            component_meta = model_metadata.for_component(i)  # 返回指定组件配置字典，这里可加入组件默认配置
            component = component_builder.load_component(
                component_meta, model_metadata.model_dir, model_metadata, **context  # 返回该组件最后组装的对象
            )
            try:
                updates = component.provide_context()  # 提供组件的信息
                if updates:
                    context.update(updates)
                pipeline.append(component)
            except components.MissingArgumentError as e:
                raise Exception(
                    "Failed to initialize component '{}'. "
                    "{}".format(component.name, e)
                )

        return Interpreter(pipeline, context, model_metadata)

    def __init__(
        self,
        pipeline: List[Component],
        context: Optional[Dict[Text, Any]],
        model_metadata: Optional[Metadata] = None,
    ) -> None:

        self.pipeline = pipeline
        self.context = context if context is not None else {}
        self.model_metadata = model_metadata

    def parse(
        self,
        text: Text,
        time: Optional[datetime.datetime] = None,
        only_output_properties: bool = True,
    ) -> Dict[Text, Any]:
        """解析输入文本，将其分类并返回管道的最终结果。
        管道结果通常包含意图和实体。
        """

        if not text:
            # 并非所有组件都能够处理空字符串。 所以我们需要防止这种情况...
            # 默认返回并不包含所有组件的所有输出属性。最后，所有人不应该在最开始传递一个空字符串。
            output = self.default_output_attributes()
            output["text"] = ""
            return output

        # message对象封装
        message = Message(text, self.default_output_attributes(), time=time)

        # pipeline各个组件预测：预测信息追加到message对象中
        for component in self.pipeline:
            component.process(message, **self.context)

        # 输出信息格式
        output = self.default_output_attributes()
        output.update(message.as_dict(only_output_properties=only_output_properties))
        return output
